class NetworkView extends View {

  Dot[] dots = new Dot[0];
  Arrow[] arrows = new Arrow[0];
  Statevar[] vars = new Statevar[0];
  Flux[] fluxes = new Flux[0];
  float varScale, fluxScale;
  String lastVar = "";
  int currentCell = 0; // in a more-than-one-cell Environment, change this to change what cell the network diagram shows
  boolean selectable = false;

  NetworkView() {
    shortname = "network";
  }

  NetworkView(float varScale, float fluxScale) {
    shortname = "network";
    this.varScale = varScale;
    this.fluxScale = fluxScale;
  }  

  Dot addDot(Statevar V, float xcen, float ycen, color col) {
    Dot D = new Dot(V.shortname, xcen, ycen, col, varScale, this);
    D.selectable = selectable;
    D.units = V.units;
    dots = (Dot[])append(dots, D);
    vars = (Statevar[])append(vars, V);
    return D;
  }
  
  Dot addDot_polar(Statevar V, float R, float theta, color col) {
    float x = R*cos(theta*PI/180);
    float y = R*sin(theta*PI/180);
    return addDot(V, x, y, col);
  }

  Arrow addArrow(Flux F, color col, float angle) {
    Arrow A = new Arrow(F.shortname, findDot(F.from.shortname), findDot(F.to.shortname), col, angle, fluxScale, this);
    arrows = (Arrow[])append(arrows, A);
    fluxes = (Flux[])append(fluxes, F);
    return A;
  }
  
  Arrow addArrow(Flux F, float dx, float dy, color col, float angle) { // use this one if one endpoint of the flux is null
    Arrow A;
    if (F.from != null) {
      A = new Arrow(F.shortname, findDot(F.from.shortname), dx, dy, col, angle, fluxScale, this);
    } else {
      A = new Arrow(F.shortname, dx, dy, findDot(F.to.shortname), col, angle, fluxScale, this);
    }      
    arrows = (Arrow[])append(arrows, A);
    fluxes = (Flux[])append(fluxes, F);
    return A;
  }

  Dot findDot(String S) { // searches the internalNames first, then the (display) names
    for (int i=0; i<dots.length; i++) if (dots[i].internalName.equals(S)) return dots[i];
    for (int i=0; i<dots.length; i++) if (dots[i].name.equals(S)) return dots[i];
    return null;
  }
  
  boolean update() {
    updateArrows();
    return updateDots();
  }
  
  boolean updateDots() {
    boolean result = false;
    for (int i=0; i<dots.length; i++) {
      boolean clicked = dots[i].update(vars[i].current[currentCell]);
      if (clicked) {
        lastVar = dots[i].internalName;
        result = true;
      }
    }
    return result;
  }
  
  void updateArrows() {
    for (int i=0; i<arrows.length; i++) {
      arrows[i].update(fluxes[i].current[currentCell]);
      arrows[i].draw();
    }
  }


  boolean offerMousePress() {
    boolean captured = false;
    for (int i=0; i<dots.length && !captured; i++) {
      captured = dots[i].selectable && dots[i].offerMousePress();
    }
    return captured;
  }
  
}



// -------------------------------------------------------------------------------
//                           network diagram elements
// -------------------------------------------------------------------------------



class Dot extends Button {

  float dataScale;
  float xcen, ycen; // normalized units, -1..1
  float x0,y0,radius; // screen units
  float origXcen, origYcen; // in case we want to change these to something new and then change them back to a home value
  View view;
  Axes ax;
  Stylesheet styles;
  color col;
  String name, internalName;
  boolean selectable = false;
  float value;
  String units = "";
  
  Dot(String name, float xcen, float ycen, color col, float dataScale, View view) {
    this.internalName = name;
    this.name = name;
    this.xcen = xcen;
    this.ycen = ycen;
    this.col = col;
    this.dataScale = dataScale;
    this.view = view;
    ax = view.ax;
    styles = view.styles;
    origXcen = xcen;
    origYcen = ycen;
  }

  boolean update(float val) {
    value = val;
    radius = constrain(sqrt(abs(val/dataScale)) * ax.width()/3, 1, ax.width()/3);
    x0 = ax.x2scr(xcen);
    y0 = ax.y2scr(ycen);
    return update(); // the Button version, which also calls draw()
  }
  
  void draw() {
    pushStyle();
    if (selectable && pressed) {
      strokeWeight(3);
      stroke(127);
    } else if (selectable && (over() || alwaysAwake)) {
      strokeWeight(3);
      stroke(255);
    } else {
      noStroke();
    }
    fill(col);
    ellipseMode(CENTER);
    ellipse(x0, y0, radius*2, radius*2);
    fill(lighten(lighten(col)));
    if (1.2*radius >= styles.smallFontSize) {
      styles.setFont(min(1.2*radius, styles.maxFontSize));
      textAlign(CENTER);
      text(name, x0, y0 + textAscent()/2);
    }
    if (over()) {
      annotate(name + ": " + round(value*100)/100. + " " + units);
    }
    popStyle();
  }
  
  boolean over() {
    return dist(mouseX,mouseY,x0,y0) < max(radius, 5);
  }
  
  void annotate(String S) {
    pushStyle();
    styles.setFont(styles.normalFontSize);
    fill(styles.labelColor);
    noStroke();
    textAlign(LEFT);
//    text(" " + S, mouseX, mouseY);
    text(" " + S, x0+radius, y0);
    popStyle();
  }
  
}



// ------------------------------------------------------------------------------------------------------------------



class Arrow {
  
  float angle, dataScale;
  Dot from,to;
  float xfrom, yfrom, xto, yto; // normalized units
  float x0,y0,x1,y1,weight; // screen units
  int dir;
  float[] displayRect;
  color col;
  String name;
  float arrowheadPos = 0.5;
  Axes ax;
  
  Arrow(String name,                                                       color col, float angle, float dataScale, View view) {
    this.name = name;
    this.col = col;
    this.angle = angle;
    this.dataScale = dataScale;
    ax = view.ax;
  }
  
  Arrow(String name,   Dot from,                   Dot to,                 color col, float angle, float dataScale, View view) {
    this(name, col, angle, dataScale, view);
    this.from = from;
    this.to = to;
  }
  
  Arrow(String name,   float xfrom, float yfrom,   Dot to,                 color col, float angle, float dataScale, View view) {
    this(name, col, angle, dataScale, view);
    setFrom(xfrom, yfrom);
    this.to = to;
  }
  
  Arrow(String name,   Dot from,                   float xto, float yto,   color col, float angle, float dataScale, View view) {
    this(name, col, angle, dataScale, view);
    setTo(xto, yto);
    this.from = from;
  }
  
  void setFrom(float x, float y) {
    from = null;
    xfrom = x;
    yfrom = y;
  }
  
  void setTo(float x, float y) {
    to = null;
    xto = x;
    yto = y;
  }

  void update(float val) {
    weight = constrain(abs(val) / dataScale * 12, 0.01, 12);
    if (val > 0) {dir = 1;} else {dir = -1;}
    if (from != null) {
      x0 = from.x0;
      y0 = from.y0;
    } else {
      x0 = ax.x2scr(xfrom);
      y0 = ax.y2scr(yfrom);
    }
    if (to != null) {
      x1 = to.x0;
      y1 = to.y0;
    } else {
      x1 = ax.x2scr(xto);
      y1 = ax.y2scr(yto);
    }
  }
  
  void draw() {
    pushStyle();
    if (isnan(x0+y0+x1+y1)) return;
    strokeWeight(weight);
    stroke(col);
    noFill();
    float[][] vertices = fluxArc(x0, y0, x1, y1, angle*PI/180);
    fill(col);
    noStroke();
    arrowhead(vertices, arrowheadPos, weight*5, dir);
    popStyle();
  }
  
  boolean over() {
    return false;
  }
  
  void annotate(String S) {
  }
  
}



// arc and arrowhead ---------------------------------------------------------------------------------------------------------------



float[][] fluxArc(float x0, float y0, float x1, float y1, float ang) {
  return fluxArc(x0,y0,x1,y1,ang,60);
}

float[][] fluxArc(float x0, float y0, float x1, float y1, float ang, int resolution) {
  // draws an arc from (x0,y0) to (x1,y1) that spans _ang_ degrees of a circle.
  float small = 1e-6;
  
  float dx,dy,D,m,xc,yc,xm,ym,angthing,th0m,th1m,dangm;
  float R,thStart,thEnd,mindist,th;
  int j,i;
  float[][] th0,th1,vertices;
  
  ang=-ang; // because y axis is inverted
  angthing = sqrt(1+cos(ang))/sqrt(1-cos(ang));
  if (abs(ang)<small) { // if ang ~ 0, just draw a straight line
    line(x0,y0,x1,y1);
    vertices = new float[2][2];
    vertices[0][0] = x0; vertices[1][0] = x1;
    vertices[0][1] = y0; vertices[1][1] = y1;
  } else {
    // find center of circle (xc,yc) relative to midpoint of starting points (xm,ym).
    // at first, there are two candidates (xm+xc,ym+yc), (xm-xc,ym-yc)
    xm = ((float)x0+(float)x1)/2;
    ym = ((float)y0+(float)y1)/2;
    dx = x1-x0;
    dy = y1-y0;
    D = sqrt(dx*dx+dy*dy);
    m = dy/dx;
    if (abs(m) > (1/small)) { // endpoints on vertical line
      xc = -0.5 * angthing * D;
      yc = 0;
    } else if (abs(m) < small) { // endpoints on horizontal line
      xc = 0;
      yc = 0.5 * angthing * D;
    } else {
      xc = -0.5 * m / sqrt(m*m+1) * angthing * D;
      yc = (-1/m) * xc; 
    }
    // now pick one of the candidate centers such that (x1,y1) is a smaller angle off it than (x0,y0)
    // (if ang > 0). There is probably a better way to this.
    th0m = atan2(y0-ym+yc,x0-xm+xc); // angle from (x0,y0) to (xm-xc,ym-yc)
    th1m = atan2(y1-ym+yc,x1-xm+xc);
    if (th0m<0) {th0m = th0m + 2*PI;}
    if (th1m<0) {th1m = th1m + 2*PI;}
    dangm = th0m-th1m;
    if (dangm>PI) {dangm = dangm - 2*PI;}
    if (dangm<-PI) {dangm = dangm + 2*PI;}
    if ((dangm>0 && ang>0) || (dangm<0 && ang<0)) {xc = -xc; yc = -yc;}
    // polar coords of points along the arc
    R = sqrt((y1-ym-yc)*(y1-ym-yc)+(x1-xm-xc)*(x1-xm-xc));
    // find the shortest way around the circle from th0 to th1: start with all options
    // (this is very elegant in Matlab)
    thStart = atan2(y0-ym-yc,x0-xm-xc);
    thEnd = atan2(y1-ym-yc,x1-xm-xc);
    th0 = new float[3][3]; th1 = new float[3][3];
    for (i=0; i<3; i++) {
      th0[0][i] = thStart - 2*PI;
      th0[1][i] = thStart;
      th0[2][i] = thStart + 2*PI;
      th1[i][0] = thEnd - 2*PI;
      th1[i][1] = thEnd;
      th1[i][2] = thEnd + 2*PI;
    }
    mindist = (1./small);
    for (j=0; j<3; j++) {
      for (i=0; i<3; i++) {
        if (abs(th1[j][i] - th0[j][i]) < mindist) {
          mindist = abs(th1[j][i] - th0[j][i]);
          thStart = th0[j][i];
          thEnd = th1[j][i];
        }
      }
    }
    // now assemble line from (R,th)
    vertices = new float[resolution][2];
    noFill();
    beginShape();
    for (i=0; i<resolution; i++) {
      th = thStart + ((float)i/((float)resolution-1)*(thEnd-thStart));
      vertices[i][0] = xm+xc+R*cos(th);
      vertices[i][1] = ym+yc+R*sin(th);
      vertex(vertices[i][0],vertices[i][1]);
    }
    endShape();
  }
  return vertices;
}


void arrowhead(float[][] vertices, float pos, float L, int dir) {
  // draws an arrowhead at fractional position _pos_ along the line (vertices[:][0],vertices[:][1]).
  // dir=1 means forward-facing, dir=-1 means backward facing.
  // L is the length of the arrowhead in pixels.
  float arrowheadangle = 35;
  float baseindent = 0.25;
  float W = 2*L*tan(arrowheadangle/180*PI/2);
  
  int x_ = 0; int y_ = 1; float small = 1e-6;
  int ilen, i, di;
  float x0, y0, xp, yp, xa1, xa2, xb, xb1, xb2, ya1, ya2, yb, yb1, yb2, xl, xl1, xl2, yl, yl1, yl2;
  float len, m, sign_x, sign_y, Dsq1, Dsq2;
  
  // pick a point on the line for the arrow tip (x0,y0) and a point back along the line (xp,yp) to
  // calculate the local slope from
  ilen = vertices.length;
  i = constrain(round(pos*(ilen-1)),0,ilen-1);
  x0 = vertices[i][x_]; y0 = vertices[i][y_];
  len = 0;
  for (int j=0; j<ilen-1; j++) {
    len = len + sqrt(sq(vertices[j+1][0]-vertices[j][0]) + sq(vertices[j+1][1]-vertices[j][1]));
  }
  di = round(L*(1-baseindent)/len*ilen);
  if (dir==1) { // forward-pointing
    if (i==0) {
      xp = x0 - (vertices[constrain(di,1,ilen-1)][x_] - vertices[0][x_]);
      yp = y0 - (vertices[constrain(di,1,ilen-1)][y_] - vertices[0][y_]);
    } else {
      xp = vertices[constrain(i-di,0,i-1)][x_];
      yp = vertices[constrain(i-di,0,i-1)][y_];
    }
  } else { // backward-pointing
    if (i==ilen-1) {
      xp = x0 + (vertices[ilen-1][x_] - vertices[constrain(ilen-1-di,0,ilen-2)][x_]);
      yp = y0 + (vertices[ilen-1][y_] - vertices[constrain(ilen-1-di,0,ilen-2)][y_]);
    } else {
      xp = vertices[constrain(i+di,i+1,ilen-1)][x_];
      yp = vertices[constrain(i+di,i+1,ilen-1)][y_];  
    }
  }
  
  // base corners of arrowhead (xa1,ya1), (xa2, ya2)
  // indented base point (xb,yb)
  m = ((float)yp-(float)y0)/((float)xp-(float)x0);
  sign_y = (yp-y0) / abs(yp-y0);
  sign_x = (xp-x0) / abs(xp-x0);
  if (abs(m) > (1./small)) {
    xa1 = -(W/2)+x0;
    xa2 = (W/2)+x0;
    ya1 = L * sign_y + y0;
    ya2 = ya1;
    xb = x0;
    yb = y0 + (1-baseindent) * L * sign_y;
  } else if (abs(m) < small) {
    xa1 = L * sign_x + x0;
    xa2 = xa1;
    ya1 = -(W/2)+y0;
    ya2 = (W/2)+y0;
    xb = x0 + (1-baseindent) * L * sign_x;
    yb = y0;
  } else {
    float Ls = L / sqrt(m*m+1);
    xl1 = Ls + x0;
    xl2 = -Ls + x0;
    yl1 = m * (xl1-x0) + y0;
    yl2 = m * (xl2-x0) + y0;
    Dsq1 = (xl1-xp)*(xl1-xp) + (yl1-yp)*(yl1-yp);
    Dsq2 = (xl2-xp)*(xl2-xp) + (yl2-yp)*(yl2-yp);
    if (Dsq1<Dsq2) {xl = xl1; yl = yl1;} else {xl = xl2; yl = yl2;}
    xa1 = -(W / 2 / sqrt(1+1./(m*m))) + xl;
    xa2 = -(xa1-xl) + xl;
    ya1 = (-1/m) * (xa1-xl) + yl;
    ya2 = (-1/m) * (xa2-xl) + yl;
    xb1 = -((1-baseindent)*Ls) + x0;
    xb2 = -(xb1-x0) + x0;
    yb1 = m*(xb1-x0) + y0;
    yb2 = m*(xb2-x0) + y0;
    Dsq1 = (xb1-xp)*(xb1-xp) + (yb1-yp)*(yb1-yp);
    Dsq2 = (xb2-xp)*(xb2-xp) + (yb2-yp)*(yb2-yp);
    if (Dsq1<Dsq2) {xb = xb1; yb = yb1;} else {xb = xb2; yb = yb2;}
  }
  // draw it
  beginShape();
  vertex(x0,y0);
  vertex(xa1,ya1);
  vertex(xb,yb);
  vertex(xa2,ya2);
  endShape(CLOSE);
}
